/*
 * Copyright 2011 Marcos Fabio Pereira
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.moveme.network;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

import com.moveme.MoveMeListener;

/**
 * This class is the one that execute calls to the MoveMe server.
 * 
 * @author Marcos Fabio Pereira
 * 
 */
public class TcpClient {

    /**
     * Move Me server address.
     */
    private String moveMeServer;

    /**
     * Move Me server port.
     */
    private int moveMePort;

    /**
     * Socket channel.
     */
    private SocketChannel socketChannel;

    /**
     * UDP client.
     */
    private final UdpClient udpClient;

    /**
     * True if the Tcp Client is connected to the Move Me Server.
     */
    private boolean connected;

    /**
     * The constructor.
     * 
     * @param moveMeServer
     *            MoveMe server host.
     * @param moveMePort
     *            MoveMe server port.
     * @param localPort
     *            UDP client local port.
     * @param listener
     *            Listener to process the received data.
     * @throws IOException
     *             Exception when a network error happens.
     */
    public TcpClient(String moveMeServer, int moveMePort, int localPort,
            MoveMeListener listener) throws IOException {
        this.connected = false;
        this.moveMeServer = moveMeServer;
        this.moveMePort = moveMePort;
        this.udpClient = new UdpClient(localPort, listener);
    }

    /**
     * Init command.
     * 
     */
    public void init() {
        InetSocketAddress socketAddress = new InetSocketAddress(moveMeServer,
                moveMePort);
        try {
            this.socketChannel = SocketChannel.open();
            this.socketChannel.connect(socketAddress);
            this.connected = true;
            new Thread(this.udpClient).start();
            int[] payload = { this.udpClient.getLocalPort() };
            this.sendCommand(Protocol.INIT, payload);
        } catch (IOException e) {
            this.udpClient.udpClientListener.notifyError(e);
        }
    }

    /**
     * Pause command.
     */
    public void pauseState() {
        this.sendCommand(Protocol.PAUSE_STATE);
    }

    /**
     * Resume command.
     */
    public void resumeState() {
        this.sendCommand(Protocol.RESUME_STATE);
    }

    /**
     * Set delay command.
     * 
     * @param stateDelay
     *            Delay in milliseconds.
     */
    public void setStateDelay(int stateDelay) {
        int[] payload = { stateDelay };
        this.sendCommand(Protocol.SET_STATE_DELAY, payload);
    }

    /**
     * Config camera command.
     * 
     * @param maxExposure
     *            Max exposure.
     * @param imageQuality
     *            Image quality.
     */
    public void configCamera(int maxExposure, float imageQuality) {
        int[] payload = { maxExposure, Float.floatToIntBits(imageQuality) };
        this.sendCommand(Protocol.CONFIG_CAMERA, payload);
    }

    /**
     * Calibrate a move controller.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void calibrate(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.CALIBRATE, payload);
    }

    /**
     * Laser left plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void laserLeftPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.LASER_LEFT_PLANE, payload);
    }

    /**
     * Laser right plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void laserRightPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.LASER_RIGHT_PLANE, payload);
    }

    /**
     * Laser bottom plane.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void laserBottomPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.LASER_BOTTOM_PLANE, payload);
    }

    /**
     * Laser top plane.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void laserTopPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.LASER_TOP_PLANE, payload);
    }

    /**
     * Turn on laser command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void turnonLaser(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.TURNON_LASER, payload);
    }

    /**
     * Turn off laser command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void turnoffLaser(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.TURNOFF_LASER, payload);
    }

    /**
     * Reset command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void reset(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.RESET, payload);
    }

    /**
     * Position left plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void positionLeftPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.POSITION_LEFT_PLANE, payload);
    }

    /**
     * Position right plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void positionRightPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.POSITION_RIGHT_PLANE, payload);
    }

    /**
     * Position bottom plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void positionBottomPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.POSITION_BOTTOM_PLANE, payload);
    }

    /**
     * Position top plane command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void positionTopPlane(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.POSITION_TOP_PLANE, payload);
    }

    /**
     * Turn on position command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void turnonPosition(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.TURNON_POSITION, payload);
    }

    /**
     * Turn off position command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     */
    public void turnoffPosition(int moveControllerId) {
        int[] payload = { moveControllerId };
        this.sendCommand(Protocol.TURNOFF_POSITION, payload);
    }

    /**
     * Set sphere color command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     * @param r
     *            Red color (0..1)
     * @param g
     *            Green color (0..1)
     * @param b
     *            Blue color (0..1)
     */
    public void setSphereColor(int moveControllerId, float r, float g, float b) {
        int[] color = { moveControllerId, Float.floatToIntBits(r),
                Float.floatToIntBits(g), Float.floatToIntBits(b) };
        this.sendCommand(Protocol.SET_SPHERE_COLOR, color);
    }

    /**
     * Set rumble command.
     * 
     * @param moveControllerId
     *            The move controller id (0..3).
     * @param rumble
     *            The vibration intensity (0..255)
     */
    public void setRumble(int moveControllerId, int rumble) {
        int[] payload = { moveControllerId, rumble };
        this.sendCommand(Protocol.SET_RUMBLE, payload);
    }

    /**
     * Set sphere track color command.
     * 
     * @param moveControllerId0
     *            The move controller id (0..3).
     * @param moveControllerId1
     *            The move controller id (0..3).
     * @param moveControllerId2
     *            The move controller id (0..3).
     * @param moveControllerId3
     *            The move controller id (0..3).
     */
    public void setSphereTrackcolor(int moveControllerId0,
            int moveControllerId1, int moveControllerId2, int moveControllerId3) {
        int[] payload = { moveControllerId0, moveControllerId1,
                moveControllerId2, moveControllerId3 };
        this.sendCommand(Protocol.SET_SPHERE_TRACKCOLOR, payload);
    }

    /**
     * Set frame delay command.
     * 
     * @param frameDelay
     *            The frame delay.
     */
    public void setFrameDelay(int frameDelay) {
        int[] payload = { frameDelay };
        this.sendCommand(Protocol.SET_FRAME_DELAY, payload);
    }

    /**
     * Set slices command.
     * 
     * @param slices
     *            Slices.
     */
    public void setSlices(int slices) {
        if (slices > 1) {
            this.udpClient.udpClientListener
                    .notifyError(new Throwable(
                            "There is no support for more than one slice yet. Sorry :(."));
        } else {
            int[] payload = { slices };
            this.sendCommand(Protocol.SET_SLICES, payload);
        }
    }

    /**
     * Pause frame command.
     */
    public void pauseFrame() {
        this.sendCommand(Protocol.PAUSE_FRAME);
    }

    /**
     * Resume frame command.
     */
    public void resumeFrame() {
        this.sendCommand(Protocol.RESUME_FRAME);
    }

    /**
     * Sends a generic command to the MoveMe server.
     * 
     * @param command
     *            The desired command.
     * @param payload
     *            The payload to send.
     */
    private void sendCommand(int command, int[] payload) {
        if (this.isConnected()) {
            ByteBuffer byteBuffer = ByteBuffer
                    .allocate(4 + 4 + (payload.length * 4));
            byteBuffer.putInt(command);
            byteBuffer.putInt(payload.length * 4);
            for (int i = 0; i < payload.length; i++) {
                byteBuffer.putInt(payload[i]);
            }
            byteBuffer.position(0);
            try {
                this.socketChannel.write(byteBuffer);
            } catch (IOException e) {
                this.udpClient.getListener().notifyError(e);
            }
        }
    }

    private boolean isConnected() {
        return connected;
    }

    /**
     * Sends a generic command to the MoveMe server. Without payload.
     * 
     * @param command
     *            The desired command.
     */
    private void sendCommand(int command) {
        int[] payload = {0};
        this.sendCommand(command, payload);
    }

}
